<?php

namespace App\CodisCache;

use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Static_;

/**
 * @method static string|bool|array get(string $key)
 * @method static bool set(string $key, string | object | array $data, int $ttl = 86400)
 * @method static bool setnx(string $key, string | object | array $data, int $ttl = 86400)
 * @method static bool del(string $key)
 * @method static int hSet(string $key, string $subKey, string $data, $ttl = 86400)
 * @method static string hGet(string $key, string $subKey)
 * @method static array hGetAll(string $key)
 * @method static int hMset(string $key, array $value, int $ttl)
 * @method static int hDel(string $key, string $hashKey1, string $hashKey2 = null, string $hashKeyN = null)
 * @method static int incr(string $key, int $offset = 1, int $ttl = 86400)
 * @method static int luaIncr(string $key, int $offset = 1, int $ttl = 86400)
 * @method static int hincr(string $key, string $subKey, int $offset = 1, int $ttl = 86400)
 * @method static int hIncrBy($key, $hashKey, $value)
 * @method static int hLen(string $key)
 * @method static int hExists(string $key, string $subKey)
 * @method static int exists(string $key)
 * @method static int lPush(string $key, string $value, int $ttl = 86400)
 * @method static int rPush(string $key, string $value, int $ttl = 86400)
 * @method static int mRPush(string $key, string $values, int $ttl = 86400)
 * @method static int mLPush(string $key, string $values, int $ttl = 86400)
 * @method static int zAdd(string $key, int $score, string $value)
 * @method static int mZAdd(string $key, array $members, $ttl)
 * @method static int zRem(string $key, string $value)
 * @method static array zRange(string $key, int $start, int $end, $withscore = null)
 * @method static array hKeys(string $key)
 * @method static array lRange(string $key, int $start, int $end)
 * @method static int hVals(string $key)
 * @method static int decrBy(string $key, int $offset)
 * @method static int decr(string $key, int $offset = 1)
 * @method static array hMget(string $key, array $idsArr)
 * @method static int zIncrBy(string $key, int $score, $value)
 * @method static expire(string $key, int $tll = 86400)
 * @method static lLen(string $key)
 * @method static rPop(string $key)
 * @method static lPop(string $key)
 * @method static sAdd(string $key, string $value)
 * @method static sRem(string $key, string $value)
 * @method static sRandMember(string $key)
 * @method static sMembers(string $key)
 * @method static sIsMember(string $key, string $value)
 * @method static sMove(string $key, string $key, string $value)
 * @method static array|bool getMetaData(string $key)
 * @method static mGet(array $data, $notCombine = 0)
 * @method static mSet(array $data, int $ttl = 86400)
 * @method static mSetNX(array $data, int $ttl = 86400)
 * @method static zRevRangeByScore($key, $start, $end, array $option = [])
 * @method static zRangeByScore($key, $start, $end, array $option = [])
 * @method static zScore($key, $member)
 * @method static zRevRange($key, $start, $stop, $withscore = null)
 */
class CodisCache
{
    /**
     * @var \Redis $redisCodisHandel
     */
    public static $redisCodisHandel;

    /**
     * 集群前缀
     */
    const CACHE_PRE = 'wx_';

    /**
     * 1分钟
     */
    const ONE_MINUTE = 60;

    /**
     * 2分钟
     */
    const TWO_MINUTE = 120;

    /**
     * 5分钟
     */
    const FIVE_MINUTE = 300;
    /**
     * 10分钟
     */
    const TEN_MINUTE = 600;

    /**
     * 20分钟
     */
    const TWENTY_MINUTE = 1200;

    /**
     * 30分钟或者半个小时
     */
    const THIRTY_MINUTE = 1800;

    /**
     * 1小时
     */
    const ONE_HOUR = 3600;

    /**
     * 2小时
     */
    const TWO_HOUR = 7200;

    /**
     * 3小时
     */
    const THREE_HOUR = 10800;

    /**
     * 12小时或者半天
     */
    const TWELVE_HOUR = 43200;

    /**
     * 1天
     */
    const ONE_DAY = 86400;

    /**
     * 2天
     */
    const TWO_DAY = 172800;

    /**
     * 3天
     */
    const THREE_DAY = 259200;

    /**
     * 4天
     */
    const FOUR_DAY = 345600;

    /**
     * 5天
     */
    const FIVE_DAY = 432000;

    /**
     * 7天
     */
    const SEVEN_DAY = 604800;

    /**
     * 10天
     */
    const TEN_DAY = 864000;

    /**
     * 30天或者1个月
     */
    const ONE_MONTH = 2592000;

    /**
     * 40天
     */
    const FORTY_DAY = 3456000;

    /**
     * 3个月或者90天
     */
    const THREE_MONTH = 7776000;

    /**
     * 6个月半年或180天
     */
    const SIX_MONTH = 15552000;

    /**
     * 一年365天
     */
    const ONE_YEAR = 31536000;


    /**
     * 获取Redis句柄
     * @return bool|mixed
     */
    protected static function _handle()
    {

        if (empty(self::$redisCodisHandel)) {
            try {
                $config = config('redis.redisCodisBusiness');
                self::$redisCodisHandel = new MyRedisCodis();
                self::$redisCodisHandel->pconnect($config['host'], $config['port']);
                if (isset($config['password'])) {
                    self::$redisCodisHandel->auth($config['password']);
                }
            } catch (\Exception $e) {
                self::$redisCodisHandel = false;
            }
        }
        return;
    }

    public function __call($name, $arguments)
    {
        self::_handle();
        $reConnect = false;
        while (1) {
            if (!empty(self::$redisCodisHandel) && method_exists(self::$redisCodisHandel, $name)) {
                try {
                    $res = call_user_func_array([
                        self::$redisCodisHandel,
                        $name,
                    ], $arguments);

                    return $res;
                } catch (\Exception $e) {
                    if ($reConnect) {
                        return false;
                    }
                    self::$redisCodisHandel = null;
                    self::_handle();
                    $reConnect = true;
                    continue;
                }
            } else {
                return false;
            }
        }
        return false;
    }

    public static function __callStatic($name, $arguments)
    {
        self::_handle();
        $reConnect = false;
        while (1) {
            if (!empty(self::$redisCodisHandel) && method_exists(self::$redisCodisHandel, $name)) {
                try {
                    $res = call_user_func_array([
                        self::$redisCodisHandel,
                        $name,
                    ], $arguments);

                    return $res;
                } catch (\Exception $e) {
                    if ($reConnect) {
                        return false;
                    }
                    self::$redisCodisHandel = null;
                    self::_handle();
                    $reConnect = true;
                    continue;
                }
            } else {
                return false;
            }
        }
        return false;
    }

    /**
     * 获取键值,第一个参数是键值,其他参数跟随
     */
    public static function getKey()
    {
        $arr = func_get_args();
        $arr[0] = self::CACHE_PRE . $arr[0];
        return call_user_func_array('sprintf', $arr);
    }

    /**
     * 为mGet做准备,变成数组键值
     * @param string $key 键值
     * @param array $arr 匹配的数组队
     * @return array
     */
    public static function getMoreKey($key, $arr)
    {
        $tmpArr = [];
        foreach ($arr as $v) {
            if (!is_array($v)) {
                $v = [$v];
            }

            $tmpArr['keyGroup'][] = call_user_func_array('self::getKey', array_merge([$key], $v));
        }
        $tmpArr['idGroup'] = $arr;
        return $tmpArr;
    }

    /**
     * 获取今日剩余的秒数
     * @return false|int
     */
    public static function getTodayLeft()
    {
        return strtotime('tomorrow') - time();
    }

    /**
     * 构造批量操作redis Key，比如mSet, mSetNx
     * @param $key
     * @param $keyPairList
     * @param $values
     * @return array
     */
    public static function buildMSetData($key, $keyPairList, $values)
    {
        if (count($keyPairList) !== count($values)) {
            throw new \InvalidArgumentException('key and values not match');
        }

        $arr = [];
        foreach ($keyPairList as $index => $keyPair) {
            $realKey = self::getKey($key, ...$keyPair);
            $arr[$realKey] = $values[$index];
        }
        return $arr;
    }

}

/**
 * Class myRedisCluster
 * @package App\Cache
 */
class MyRedisCodis extends \Redis
{

    /**
     * 序列化操作
     * @param $data
     * @return string
     */
    private function serialize($data)
    {
        if (is_array($data) || is_object($data) || is_bool($data) || is_null($data)) {
            $data = serialize($data);
        }
        return $data;
    }

    /**
     * 反序列化，判断操作
     * @param $data
     * @return bool|mixed|null
     */
    private function unserialize($data)
    {
        if (is_string($data)) {
            if ($data == "N;") return null;
            if (substr($data, 1, 1) == ":") {
                if ($data == "b:0;") return false;
                $_data = @unserialize($data);
                if ($_data !== false) {
                    return $_data;
                }
            }
        }

        return $data;
    }

    /**
     * 批量设置
     * @param array $array [key1 => value1, key2 => value2]
     * @param       $ttl
     * @return bool|void
     */
    public function mSet(array $array, $ttl = 86400)
    {
        // serialize data
        foreach ($array as $key => $value) {
            $array[$key] = $this->serialize($value);
        }

        $ret = parent::mset($array);
        $pipeline = parent::multi(self::PIPELINE);
        foreach ($array as $key => $v) {
            $pipeline->expire($key, $ttl);
        }

        $pipeline->exec();
        return $ret;
    }

    /**
     * 批量设置，如果key存在，不设置
     * @param array $array [key1 => value1, key2 => value2]
     * @param int   $ttl
     * @return int
     */
    public function mSetNx(array $array, $ttl = 86400)
    {
        // serialize data
        foreach ($array as $key => $value) {
            $array[$key] = $this->serialize($value);
        }

        $ret = parent::msetnx($array);
        $pipeline = parent::multi(self::PIPELINE);
        foreach ($array as $key => $v) {
            $pipeline->expire($key, $ttl);
        }

        $pipeline->exec();
        return $ret;
    }

    /**
     * 批量获取,自动根据传进来的ID排序
     * @param array $array 包含idGroup(键),keyGroup2个组
     * @param int $notCombine 不合并
     * @return array
     */
    public function mGet(array $array, $notCombine = 0)
    {
        $list = parent::mget($array['keyGroup']);
        foreach ($list as $k => $v) {
            $list[$k] = $this->unserialize($v);
        }
        if ($notCombine) {
            return $list;
        } else {
            return array_combine($array['idGroup'], $list);
        }
    }

    //get 使用unserialize反序列化
    public function get($key)
    {
        $result = parent::get($key);
        return $this->unserialize($result);
    }

    //set 使用serialize序列化
    public function set($key, $data, $ttl = 86400)
    {
        $data = $this->serialize($data);
        if ($ttl) {
            return parent::setex($key, $ttl, $data);
        } else {
            return parent::set($key, $data);
        }
    }

    //setnx 使用serialize序列化
    public function setnx($key, $data, $ttl = 86400)
    {
        $data = $this->serialize($data);
        if ($ttl) {
            $ret = parent::setnx($key, $data);
            if ($ret) {
                parent::expire($key, $ttl);
            }
            return $ret;
        }
    }

    //hSet
    public function hSet($key, $subKey, $value, $ttl = 86400)
    {
        $value = $this->serialize($value);

        if (parent::exists($key)) {
            $ret = parent::hSet($key, $subKey, $value);
        } else {
            $ret = parent::hSet($key, $subKey, $value);
            if ($ttl) {
                parent::expire($key, $ttl);
            }
        }
        return $ret;
    }

    //hGet
    public function hGet($key, $subKey)
    {
        $result = parent::hGet($key, $subKey);
        return $this->unserialize($result);
    }

    public function hGetAll($key)
    {
        $result = parent::hGetAll($key);
        return $this->unserialize($result);
    }

    /**
     * 递增
     * @param $key
     * @param int $offset
     * @param int $ttl
     * @return mixed
     */
    function incr($key, $offset = 1, $ttl = 86400)
    {
        $incr = parent::incrBy($key, $offset);
        if ($incr == $offset) {
            parent::expire($key, $ttl);
        }
        return $incr;
    }

    /**
     * 递增lua
     * @param $key
     * @param int $offset
     * @param int $ttl
     * @return mixed
     */
    function luaIncr($key, $offset = 1, $ttl = 86400)
    {
        $script = "local x = redis.call('INCRBY',KEYS[1],ARGV[1])
            if tonumber(x) == tonumber(ARGV[1]) then
                redis.call('EXPIRE',KEYS[1],ARGV[2])
            end
            return x";
        return parent::eval($script, [$key, $offset, $ttl], 1);
    }

    /**
     * 递减
     * @param $key
     * @param int $offset
     * @param int $ttl
     * @return mixed
     */
    function decr($key, $offset = 1, $ttl = 86400)
    {
        $decr = parent::decrBy($key, $offset);
        if ($decr == -1 * $offset) {
            parent::expire($key, $ttl);
        }
        return $decr;
    }

    /**
     * 哈希递增
     * @param $key
     * @param $subKey
     * @param $offset
     * @param int $ttl
     * @return int
     */
    public function hincr($key, $subKey, $offset = 1, $ttl = 86400)
    {
        $hincr = parent::hIncrBy($key, $subKey, $offset);
        if ($hincr == $offset) { //第一次插入设置过期时间
            parent::expire($key, $ttl);
        }
        return $hincr;
    }

    /**
     * 列表推送
     * @param $key
     * @param $value
     * @param int $ttl
     * @return mixed
     */
    public function lPush($key, $value, $ttl = 86400)
    {
        if (parent::exists($key)) {
            $ret = parent::lPush($key, $value);
        } else {
            $ret = parent::lPush($key, $value);
            if ($ttl) {
                parent::expire($key, $ttl);
            }
        }
        return $ret;
    }

    /**
     * 列表推送
     * @param $key
     * @param $value
     * @param int $ttl
     * @return mixed
     */
    public function rPush($key, $value, $ttl = 86400)
    {
        if (parent::exists($key)) {
            $ret = parent::rPush($key, $value);
        } else {
            $ret = parent::rPush($key, $value);
            if ($ttl) {
                parent::expire($key, $ttl);
            }
        }
        return $ret;
    }

    /**
     * 批量插入数据到队列头部
     * @param     $key
     * @param     $values
     * @param int $ttl
     * @return bool|int
     */
    public function mLPush($key, $values, $ttl = 86400)
    {
        $ret = parent::lPush($key, ...$values);
        parent::expire($key, $ttl);

        return $ret;
    }

    /**
     * 批量插入数据到队列尾部
     * @param     $key
     * @param     $values
     * @param int $ttl
     * @return bool|int
     */
    public function mRPush($key, $values, $ttl = 86400)
    {
        $ret = parent::rPush($key, ...$values);
        parent::expire($key, $ttl);

        return $ret;
    }

    /**
     * 获取队列数据
     * @param string $key
     * @param int $start
     * @param int $end
     * @return array
     */
    public function lRange($key, $start, $end)
    {
        return parent::lRange($key, $start, $end);
    }

    /**
     * 更新过期时间
     * @param string $key
     * @param int $ttl
     * @return bool
     */
    public function expire($key, $ttl = 86400)
    {
        if ($ttl) {
            return parent::expire($key, $ttl);
        }

        return false;
    }

    /**
     * hMSet 不会序列化
     * @param string $key
     * @param array $value
     * @param int $ttl
     * @return bool
     */
    public function hMset($key, $value, $ttl = 86400)
    {
        if (parent::exists($key)) {
            $ret = parent::hMset($key, $value);
        } else {
            $ret = parent::hMset($key, $value);
            if ($ttl) {
                parent::expire($key, $ttl);
            }
        }
        return $ret;
    }

    /**
     * 批量设置有序集合
     * @param string $key
     * @param array  $members [$score1, $member1, $score2, $member2, $score3, $member3]
     * @param        $ttl
     * @return int
     */
    public function mZAdd(string $key, array $members, $ttl = 86400)
    {
        $ret = parent::zAdd($key, ...$members);
        parent::expire($key, $ttl);

        return $ret;
    }

    /**
     * @param $key
     * @return array|bool
     */
    public function getMetaData($key)
    {
        $value = $this->get($key);
        if ($value) {
            return [
                'expire' => time() + parent::ttl($key),
                'data' => $value
            ];
        }
        return FALSE;
    }
}